"""
    Git(;
        ignore=String[],
        name=nothing,
        email=nothing,
        ssh=false,
        manifest=false,
        gpgsign=false,
    )

Creates a Git repository and a `.gitignore` file.

## Keyword Arguments
- `ignore::Vector{<:AbstractString}`: Patterns to add to the `.gitignore`.
  See also: [`gitignore`](@ref).
- `name::AbstractString`: Your real name, if you have not set `user.name` with Git.
- `email::AbstractString`: Your email address, if you have not set `user.email` with Git.
- `ssh::Bool`: Whether or not to use SSH for the remote.
  If left unset, HTTPS is used.
- `manifest::Bool`: Whether or not to commit `Manifest.toml`.
- `gpgsign::Bool`: Whether or not to sign commits with your GPG key.
  This option requires that the Git CLI is installed,
  and for you to have a GPG key associated with your committer identity.
"""
@with_kw_noshow struct Git <: Plugin
    ignore::Vector{String} = String[]
    name::Union{String, Nothing} = nothing
    email::Union{String, Nothing} = nothing
    ssh::Bool = false
    manifest::Bool = false
    gpgsign::Bool = false
end

# Try to make sure that no files are created after we commit.
priority(::Git, ::typeof(posthook)) = 5

Base.:(==)(a::Git, b::Git) = all(map(n -> getfield(a, n) == getfield(b, n), fieldnames(Git)))

function gitignore(p::Git)
    ignore = copy(p.ignore)
    p.manifest || push!(ignore, "Manifest.toml")
    return ignore
end

function validate(p::Git, t::Template)
    if p.gpgsign && !git_is_installed()
        throw(ArgumentError("Git: gpgsign is set but the Git CLI is not installed"))
    end

    foreach((:name, :email)) do k
        user_k = "user.$k"
        if getproperty(p, k) === nothing && isempty(LibGit2.getconfig(user_k, ""))
            throw(ArgumentError("Git: Global Git config is missing required value '$user_k'"))
        end
    end
end

# Set up the Git repository.
function prehook(p::Git, t::Template, pkg_dir::AbstractString)
    LibGit2.with(LibGit2.init(pkg_dir)) do repo
        LibGit2.with(GitConfig(repo)) do config
            foreach((:name, :email)) do k
                v = getproperty(p, k)
                v === nothing || LibGit2.set!(config, "user.$k", v)
            end
        end
        commit(p, repo, pkg_dir, "Initial commit")
        pkg = basename(pkg_dir)
        url = if p.ssh
            "git@$(t.host):$(t.user)/$pkg.jl.git"
        else
            "https://$(t.host)/$(t.user)/$pkg.jl"
        end
        LibGit2.with(GitRemote(repo, "origin", url)) do remote
            LibGit2.add_fetch!(repo, remote, "refs/heads/master")
            LibGit2.add_push!(repo, remote, "refs/heads/master")
        end
    end
end

# Create the .gitignore.
function hook(p::Git, t::Template, pkg_dir::AbstractString)
    ignore = mapreduce(gitignore, vcat, t.plugins)
    # Only ignore manifests at the repo root.
    p.manifest || "Manifest.toml" in ignore || push!(ignore, "/Manifest.toml")
    unique!(sort!(ignore))
    gen_file(joinpath(pkg_dir, ".gitignore"), join(ignore, "\n"))
end

# Commit the files.
function posthook(p::Git, ::Template, pkg_dir::AbstractString)
    # Ensure that the manifest exists if it's going to be committed.
    manifest = joinpath(pkg_dir, "Manifest.toml")
    if p.manifest && !isfile(manifest)
        touch(manifest)
        with_project(Pkg.update, pkg_dir)
    end

    LibGit2.with(GitRepo(pkg_dir)) do repo
        LibGit2.add!(repo, ".")
        msg = "Files generated by PkgTemplates"
        v = version_of("PkgTemplates")
        v === nothing || (msg *= "\n\nPkgTemplates version: $v")
        commit(p, repo, pkg_dir, msg)
    end
end

function commit(p::Git, repo::GitRepo, pkg_dir::AbstractString, msg::AbstractString)
    if p.gpgsign
        run(pipeline(`git -C $pkg_dir commit -S --allow-empty -m $msg`; stdout=devnull))
    else
        LibGit2.commit(repo, msg)
    end
end

needs_username(::Git) = true

function git_is_installed()
    return try
        run(pipeline(`git --version`; stdout=devnull))
        true
    catch
        false
    end
end

if isdefined(Pkg, :dependencies)
    function version_of(pkg::AbstractString)
        for p in values(Pkg.dependencies())
            p.name == pkg && return p.version
        end
        return nothing
    end
else
    version_of(pkg::AbstractString) = get(Pkg.installed(), pkg, nothing)
end
